/*******************************************************************************
 * Copyright (c) 2010 Sylvain Sicard.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Sylvain Sicard - initial API and implementation
 ******************************************************************************/
package com.googlecode.ssdutils.beanconverter.asm;

import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.BEANCONVERTER_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.CLASS_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.COLLECTION_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.ITERATOR_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.MAP_ENTRY_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.MAP_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.OBJECT_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.SET_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.STRING_ICN;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.getExternalName;
import static com.googlecode.ssdutils.beanconverter.asm.InternalClassNamesUtil.getInternalName;

import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.googlecode.ssdutils.beanconverter.api.BeanConverter;
import com.googlecode.ssdutils.beanconverter.api.BeanConverterFactory;
import com.googlecode.ssdutils.beanconverter.factory.BeanConverterFactoryImpl;

/**
 * @author Sylvain Sicard
 * 
 */
public class ConverterClassGeneratorImpl<S, T> implements ClassGenerator<S, T>,
		Opcodes {

	private static final Map<String, String> primitiveOpCodes = new HashMap<String, String>();

	private static Map<String, List<String>> entitiesConvertMap = new HashMap<String, List<String>>();

	static {
		primitiveOpCodes.put("int", "I");
		primitiveOpCodes.put("long", "J");
		primitiveOpCodes.put("float", "F");
		primitiveOpCodes.put("double", "D");
		primitiveOpCodes.put("boolean", "Z");
		primitiveOpCodes.put("short", "S");
		primitiveOpCodes.put("char", "C");
	}

	public byte[] generateConverterClass(String className, Class<S> from,
			Class<T> to, S fromObject) throws Exception {

		String internalClassName = getInternalName(className);
		String classFrom = getInternalName(from);
		String classTo = getInternalName(to);

		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
		MethodVisitor mv;
		Label beginConvertLabel = new Label();
		Label endConvertLabel = new Label();

		cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, internalClassName, "L"
				+ OBJECT_ICN + ";L" + BEANCONVERTER_ICN + "<L" + classFrom
				+ ";L" + classTo + ";>;", OBJECT_ICN,
				new String[] { BEANCONVERTER_ICN });

		constructor(internalClassName, cw);
		{

			mv = initConvertMethod(classFrom, classTo, cw, beginConvertLabel);

			mv.visitTypeInsn(NEW, classTo);
			mv.visitInsn(DUP);
			mv.visitMethodInsn(INVOKESPECIAL, classTo, "<init>", "()V");
			mv.visitVarInsn(ASTORE, 2);

			// for (Field field : from.getDeclaredFields()) {
			for (Field field : fromObject.getClass().getDeclaredFields()) {
				copyField(classFrom, classTo, fromObject, mv, field);
			}

			finalyseConvertMethod(internalClassName, classFrom, classTo, mv,
					beginConvertLabel, endConvertLabel);
		}
		{
			mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC,
					"convert", "(L" + OBJECT_ICN + ";)L" + OBJECT_ICN + ";",
					null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitLineNumber(1, l0);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitVarInsn(ALOAD, 1);
			mv.visitTypeInsn(CHECKCAST, classFrom);
			mv.visitMethodInsn(INVOKEVIRTUAL, internalClassName, "convert",
					"(L" + classFrom + ";)L" + classTo + ";");
			mv.visitInsn(ARETURN);
			mv.visitMaxs(2, 2);
			mv.visitEnd();
		}
		cw.visitEnd();

		return cw.toByteArray();
	}

	/**
	 * @param classFrom
	 * @param classTo
	 * @param fromObject
	 * @param mv
	 * @param field
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 */
	private void copyField(String classFrom, String classTo, Object objectFrom,
			MethodVisitor mv, Field field) throws IllegalArgumentException,
			IllegalAccessException {
		String fieldName = getFieldName(field.getName());
		Class<?> fieldType = field.getType();
		String fieldTypeName = getInternalName(fieldType);
		field.setAccessible(true);
		String valueFieldType = getInternalName(field.get(objectFrom)
				.getClass());

		if (isCollection(fieldType)) {
			copyCollectionField(classFrom, classTo, mv, fieldName,
					fieldTypeName, valueFieldType);
		} else if (isMap(fieldType)) {
			copyMapField(classFrom, classTo, mv, fieldName, fieldTypeName,
					valueFieldType);
		} else if (isEntity(fieldType)) {
			copyEntity(classFrom, classTo, mv, fieldName, fieldTypeName);
		} else {
			copyFieldByRef(classFrom, classTo, mv, fieldName, fieldTypeName,
					fieldType.isPrimitive(), fieldType.isArray());
		}
	}

	/**
	 * @param fieldTypeName
	 * @param fieldName
	 * @param mv
	 * @param classTo
	 * @param classFrom
	 * 
	 */
	protected void copyEntity(String classFrom, String classTo,
			MethodVisitor mv, String fieldName, String fieldTypeName) {
		String suffixe = getSuffixe(classTo);
		mv.visitTypeInsn(NEW, getInternalName(BeanConverterFactoryImpl.class));
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL,
				getInternalName(BeanConverterFactoryImpl.class), "<init>",
				"()V");
		mv.visitVarInsn(ASTORE, 7);
		Label l21 = new Label();
		mv.visitLabel(l21);
		mv.visitLineNumber(64, l21);
		mv.visitVarInsn(ALOAD, 7);
		mv.visitLdcInsn(Type.getType("L" + fieldTypeName + ";"));
		mv.visitLdcInsn(Type.getType("L"
				+ convertEntityTypeName(fieldTypeName, suffixe) + ";"));
		mv.visitMethodInsn(INVOKEINTERFACE,
				getInternalName(BeanConverterFactory.class), "getConverter",
				"(L" + CLASS_ICN + ";L" + CLASS_ICN + ";)L"
						+ getInternalName(BeanConverter.class) + ";");
		mv.visitVarInsn(ASTORE, 8);
		Label l22 = new Label();
		mv.visitLabel(l22);
		mv.visitLineNumber(65, l22);
		mv.visitVarInsn(ALOAD, 8);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, classFrom, "get" + fieldName, "()L"
				+ fieldTypeName + ";");
		mv.visitMethodInsn(INVOKEINTERFACE,
				getInternalName(BeanConverter.class), "convert", "(L"
						+ OBJECT_ICN + ";)L" + OBJECT_ICN + ";");
		mv.visitTypeInsn(CHECKCAST,
				convertEntityTypeName(fieldTypeName, suffixe));
		mv.visitVarInsn(ASTORE, 9);
		Label l23 = new Label();
		mv.visitLabel(l23);
		mv.visitLineNumber(66, l23);
		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 9);
		mv.visitMethodInsn(INVOKEVIRTUAL, classTo, "set" + fieldName, "(L"
				+ convertEntityTypeName(fieldTypeName, suffixe) + ";)V");
	}

	/**
	 * @param classFrom
	 * @return
	 */
	private String getSuffixe(String className) {
		String suffixe = null;
		for (String k : entitiesConvertMap.keySet()) {
			Pattern pattern = Pattern.compile(k);
			Matcher m = pattern.matcher(getExternalName(className));
			if (m.find()) {
				suffixe = m.group(2);
				break;
			}
		}
		return suffixe;
	}

	/**
	 * @param fieldTypeName
	 * @param suffixe
	 * @return
	 */
	private String convertEntityTypeName(String fieldTypeName, String suffixe) {
		String convertedTypeName = null;
		boolean found = false;
		for (String k : entitiesConvertMap.keySet()) {
			Pattern pattern = Pattern.compile(k);
			Matcher m = pattern.matcher(getExternalName(fieldTypeName));
			if (m.find()) {
				String baseName = m.group(1);
				List<String> candidates = entitiesConvertMap.get(k);
				for (String candidate : candidates) {
					if (candidate.endsWith(suffixe)) {
						convertedTypeName = new MessageFormat(candidate)
								.format(new Object[] { baseName });
						found = true;
						break;
					}
				}
				if (found)
					break;
			}
		}
		return convertedTypeName.replaceAll("\\.", "/");
	}

	/**
	 * @param fieldType
	 * @return
	 */
	private boolean isEntity(Class<?> fieldType) {
		boolean isEntity = false;
		String typeName = fieldType.getName();
		for (String k : entitiesConvertMap.keySet()) {
			Pattern pattern = Pattern.compile(k);
			Matcher m = pattern.matcher(typeName);
			if (m.find()) {
				isEntity = true;
				break;
			}
		}
		return isEntity;
	}

	/**
	 * @param fieldType
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private boolean isMap(Class<?> fieldType) {
		boolean isCollectionOrMap = false;
		Class<Map> mapClass = Map.class;

		for (Class<?> itf : fieldType.getInterfaces()) {
			if (itf.equals(mapClass)) {
				isCollectionOrMap = true;
				break;
			}
		}
		return isCollectionOrMap;
	}

	public void setEntitiesConvertMap(
			Map<String, List<String>> entitiesConvertMap) {
		ConverterClassGeneratorImpl.entitiesConvertMap = entitiesConvertMap;
	}

	/**
	 * @param fieldType
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private boolean isCollection(Class<?> fieldType) {
		boolean isCollectionOrMap = false;
		Class<Collection> collectionClass = Collection.class;

		for (Class<?> itf : fieldType.getInterfaces()) {
			if (itf.equals(collectionClass)) {
				isCollectionOrMap = true;
				break;
			}
		}
		return isCollectionOrMap;
	}

	/**
	 * @param name
	 * @return
	 */
	private String getFieldName(String name) {
		return Character.toTitleCase(name.charAt(0)) + name.substring(1);
	}

	protected void copyCollectionField(String classFrom, String classTo,
			MethodVisitor mv, String fieldName, String fieldType,
			String valueFieldType) {

		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, classFrom, "get" + fieldName, "()L"
				+ fieldType + ";");
		mv.visitVarInsn(ASTORE, 5);

		mv.visitTypeInsn(NEW, valueFieldType);
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL, valueFieldType, "<init>", "()V");
		mv.visitVarInsn(ASTORE, 6);

		mv.visitVarInsn(ALOAD, 5);
		mv.visitMethodInsn(INVOKEINTERFACE, COLLECTION_ICN, "iterator", "()L"
				+ ITERATOR_ICN + ";");
		mv.visitVarInsn(ASTORE, 8);
		Label l16 = new Label();
		mv.visitJumpInsn(GOTO, l16);
		Label l17 = new Label();
		mv.visitLabel(l17);
		mv.visitFrame(Opcodes.F_FULL, 9, new Object[] {
				"asmifer/ConverterTest", classFrom, classTo, MAP_ICN, MAP_ICN,
				COLLECTION_ICN, COLLECTION_ICN, Opcodes.TOP, ITERATOR_ICN }, 0,
				new Object[] {});
		mv.visitVarInsn(ALOAD, 8);
		mv.visitMethodInsn(INVOKEINTERFACE, ITERATOR_ICN, "next", "()L"
				+ OBJECT_ICN + ";");
		mv.visitTypeInsn(CHECKCAST, STRING_ICN);
		mv.visitVarInsn(ASTORE, 7);

		mv.visitVarInsn(ALOAD, 6);
		mv.visitVarInsn(ALOAD, 7);
		mv.visitMethodInsn(INVOKEINTERFACE, COLLECTION_ICN, "add",
				"(Ljava/lang/Object;)Z");
		mv.visitInsn(POP);
		mv.visitLabel(l16);
		mv.visitLineNumber(54, l16);
		mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
		mv.visitVarInsn(ALOAD, 8);
		mv.visitMethodInsn(INVOKEINTERFACE, ITERATOR_ICN, "hasNext", "()Z");
		mv.visitJumpInsn(IFNE, l17);

		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 5);
		mv.visitMethodInsn(INVOKEVIRTUAL, classTo, "set" + fieldName, "(L"
				+ fieldType + ";)V");

	}

	protected void copyMapField(String classFrom, String classTo,
			MethodVisitor mv, String fieldName, String fieldType,
			String valueFieldType) {
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, classFrom, "get" + fieldName, "()L"
				+ fieldType + ";");
		mv.visitVarInsn(ASTORE, 3);

		mv.visitTypeInsn(NEW, valueFieldType);
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL, valueFieldType, "<init>", "()V");
		mv.visitVarInsn(ASTORE, 4);

		mv.visitVarInsn(ALOAD, 3);
		mv.visitMethodInsn(INVOKEINTERFACE, MAP_ICN, "entrySet", "()L"
				+ SET_ICN + ";");
		mv.visitMethodInsn(INVOKEINTERFACE, SET_ICN, "iterator", "()L"
				+ ITERATOR_ICN + ";");
		mv.visitVarInsn(ASTORE, 6);
		Label l9 = new Label();
		mv.visitJumpInsn(GOTO, l9);
		Label l10 = new Label();
		mv.visitLabel(l10);
		mv.visitFrame(Opcodes.F_FULL, 7, new Object[] {
				"asmifer/ConverterTest", classFrom, classTo, MAP_ICN, MAP_ICN,
				Opcodes.TOP, ITERATOR_ICN }, 0, new Object[] {});
		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKEINTERFACE, ITERATOR_ICN, "next", "()L"
				+ OBJECT_ICN + ";");
		mv.visitTypeInsn(CHECKCAST, MAP_ENTRY_ICN);
		mv.visitVarInsn(ASTORE, 5);

		mv.visitVarInsn(ALOAD, 4);
		mv.visitVarInsn(ALOAD, 5);
		mv.visitMethodInsn(INVOKEINTERFACE, MAP_ENTRY_ICN, "getKey", "()L"
				+ OBJECT_ICN + ";");
		mv.visitTypeInsn(CHECKCAST, STRING_ICN);
		mv.visitVarInsn(ALOAD, 5);
		mv.visitMethodInsn(INVOKEINTERFACE, MAP_ENTRY_ICN, "getValue", "()L"
				+ OBJECT_ICN + ";");
		mv.visitTypeInsn(CHECKCAST, STRING_ICN);
		mv.visitMethodInsn(INVOKEINTERFACE, MAP_ICN, "put", "(L" + OBJECT_ICN
				+ ";L" + OBJECT_ICN + ";)L" + OBJECT_ICN + ";");
		mv.visitInsn(POP);
		mv.visitLabel(l9);

		mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKEINTERFACE, ITERATOR_ICN, "hasNext", "()Z");
		mv.visitJumpInsn(IFNE, l10);

		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 4);
		mv.visitMethodInsn(INVOKEVIRTUAL, classTo, "set" + fieldName, "(L"
				+ fieldType + ";)V");

	}

	/**
	 * @param classFrom
	 * @param classTo
	 * @param mv
	 * @param isPrimitive
	 * @param isArray
	 */
	protected void copyFieldByRef(String classFrom, String classTo,
			MethodVisitor mv, String fieldName, String fieldType,
			boolean isPrimitive, boolean isArray) {
		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 1);
		String codeType = "L" + fieldType + ";";
		if (isPrimitive) {
			codeType = primitiveOpCodes.get(fieldType);
		}
		if (isArray) {
			codeType = fieldType;
		}
		mv.visitMethodInsn(INVOKEVIRTUAL, classFrom, "get" + fieldName, "()"
				+ codeType);
		mv.visitMethodInsn(INVOKEVIRTUAL, classTo, "set" + fieldName, "("
				+ codeType + ")V");
	}

	/**
	 * @param className
	 * @param classFrom
	 * @param classTo
	 * @param mv
	 * @param l0
	 * @param l4
	 */
	protected void finalyseConvertMethod(String className, String classFrom,
			String classTo, MethodVisitor mv, Label l0, Label l4) {

		mv.visitVarInsn(ALOAD, 2);
		mv.visitInsn(ARETURN);

		mv.visitLabel(l4);
		mv.visitLocalVariable("this", "L" + className + ";", null, l0, l4, 0);
		mv.visitLocalVariable("peopleA", "L" + classFrom + ";", null, l0, l4, 1);
		mv.visitLocalVariable("peopleB", "L" + classTo + ";", null, l0, l4, 2);
		mv.visitMaxs(2, 3);
		mv.visitEnd();
	}

	/**
	 * @param classFrom
	 * @param classTo
	 * @param cw
	 * @param l0
	 * @return
	 */
	protected MethodVisitor initConvertMethod(String classFrom, String classTo,
			ClassWriter cw, Label l0) {
		MethodVisitor mv;
		mv = cw.visitMethod(ACC_PUBLIC, "convert", "(L" + classFrom + ";)L"
				+ classTo + ";", null, null);
		mv.visitCode();

		mv.visitLabel(l0);
		return mv;
	}

	/**
	 * @param className
	 * @param cw
	 */
	protected void constructor(String className, ClassWriter cw) {
		MethodVisitor mv;
		{
			mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitLineNumber(24, l0);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitMethodInsn(INVOKESPECIAL, OBJECT_ICN, "<init>", "()V");
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitLineNumber(25, l1);
			mv.visitInsn(RETURN);
			Label l2 = new Label();
			mv.visitLabel(l2);
			mv.visitLocalVariable("this", "L" + className + ";", null, l0, l2,
					0);
			mv.visitMaxs(1, 1);
			mv.visitEnd();
		}
	}

}
